package com.nageoffer.shortlink.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.Week;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nageoffer.shortlink.project.common.convention.exception.ServiceException;
import com.nageoffer.shortlink.project.common.enums.VailDateTypeEnum;
import com.nageoffer.shortlink.project.dao.entity.*;
import com.nageoffer.shortlink.project.dao.mapper.*;
import com.nageoffer.shortlink.project.dto.req.ShortLinkCreateReqDTO;
import com.nageoffer.shortlink.project.dto.req.ShortLinkPageReqDTO;
import com.nageoffer.shortlink.project.dto.req.ShortLinkUpdateReqDTO;
import com.nageoffer.shortlink.project.dto.resp.ShortLinkCreateRespDTO;
import com.nageoffer.shortlink.project.dto.resp.ShortLinkGroupCountQueryRespDTO;
import com.nageoffer.shortlink.project.dto.resp.ShortLinkPageRespDTO;
import com.nageoffer.shortlink.project.service.ShortLinkService;
import com.nageoffer.shortlink.project.toolkit.HashUtil;
import com.nageoffer.shortlink.project.toolkit.LinkUtil;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import static com.nageoffer.shortlink.project.common.constant.RedisKeyConstant.*;
import static com.nageoffer.shortlink.project.common.constant.ShortLinkConstant.SHORT_LINK_REMOTE_URL;
import static com.nageoffer.shortlink.project.common.constant.StatsLocaleAmapkey.AMAP_KEY;

@Service
@Slf4j
@RequiredArgsConstructor
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO> implements ShortLinkService {


    private final RBloomFilter<String> shortLinkCreateCachePenetrationBloomFilter;
    private final ShortLinkGotoMapper shortLinkGotoMapper;
    private final StringRedisTemplate stringRedisTemplate;
    private final RedissonClient redissonClient;
    private final LinkAccessStatsMapper linkAccessStatsMapper;
    private final LinkLocaleStatsMapper linkLocaleStatsMapper;
    private final LinkOsStatsMapper linkOsStatsMapper;
    private final LinkBrowserStatsMapper linkBrowserStatsMapper;
    private final LinkDeviceStatsMapper linkDeviceStatsMapper;
    private final LinkAccessLogMapper linkAccessLogMapper;
    private final LinkNetworkStatsMapper linkNetworkStatsMapper;

    @Override
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO reqParam) {
        String ShortLinkSuffix = generateSuffix(reqParam);
        ShortLinkDO shortLinkDO = BeanUtil.toBean(reqParam, ShortLinkDO.class);
        String fullShortUrl = reqParam.getDomain() + "/" + ShortLinkSuffix;
        shortLinkDO.setFavicon(getFavicon(reqParam.getOriginUrl()));
        shortLinkDO.setShortUri(ShortLinkSuffix);
        shortLinkDO.setFullShortUrl(fullShortUrl);
        try {
            baseMapper.insert(shortLinkDO);
            shortLinkGotoMapper.insert(ShortLinkGotoDO.builder()
                    .gid(reqParam.getGid())
                    .fullShortUrl(fullShortUrl)
                    .build());
        } catch (DuplicateKeyException ex) {
            throw new ServiceException("短链接生成重复");
        }
        stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                reqParam.getOriginUrl(),
                LinkUtil.getLinkValidTime(reqParam.getValidDate()),
                TimeUnit.MINUTES);
        shortLinkCreateCachePenetrationBloomFilter.add(fullShortUrl);
        return ShortLinkCreateRespDTO.builder()
                .gid(reqParam.getGid())
                .fullShortUrl("http://" + shortLinkDO.getFullShortUrl())
                .gid(reqParam.getGid())
                .originUrl(reqParam.getOriginUrl())
                .build();
    }

    /**
     * 分组查询短链接
     */
    @Override
    public IPage<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO reqParam) {
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, reqParam.getGid())
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getEnableStatus, 0);
        IPage<ShortLinkDO> resultPage = baseMapper.selectPage(reqParam, queryWrapper);
        return resultPage.convert(each -> BeanUtil.toBean(each, ShortLinkPageRespDTO.class));
    }

    @Override
    public List<ShortLinkGroupCountQueryRespDTO> listGroupShortLinkCount(List<String> requestParam) {
        QueryWrapper<ShortLinkDO> queryWrapper = Wrappers.query(new ShortLinkDO())
                .select("gid as gid, count(*) as shortLinkCount")
                .in("gid", requestParam)
                .eq("enable_status", 0)
                .eq("del_flag", 0)
                .groupBy("gid");
        List<Map<String, Object>> shortLinkDOList = baseMapper.selectMaps(queryWrapper);
        return BeanUtil.copyToList(shortLinkDOList, ShortLinkGroupCountQueryRespDTO.class);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateShortLink(ShortLinkUpdateReqDTO reqParam) {
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, reqParam.getGid())
                .eq(ShortLinkDO::getFullShortUrl, reqParam.getFullShortUrl())
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getEnableStatus, 0);
        ShortLinkDO hasShortLinkDO = baseMapper.selectOne(queryWrapper);
        if (hasShortLinkDO == null) {
            throw new ServiceException("短链接不存在");
        }
        ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                .domain(hasShortLinkDO.getDomain())
                .shortUri(hasShortLinkDO.getShortUri())
                .favicon(hasShortLinkDO.getFavicon())
                .createdType(hasShortLinkDO.getCreatedType())
                .gid(reqParam.getGid())
                .originUrl(reqParam.getOriginUrl())
                .describe(reqParam.getDescribe())
                .validDateType(reqParam.getValidDateType())
                .validDate(reqParam.getValidDate())
                .build();
        if (Objects.equals(hasShortLinkDO.getGid(), reqParam.getGid())) {
            LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                    .eq(ShortLinkDO::getGid, reqParam.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, reqParam.getFullShortUrl())
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .set(Objects.equals(reqParam.getValidDateType(), VailDateTypeEnum.PERMANENT.getType()),
                            ShortLinkDO::getValidDate, null);
            baseMapper.update(shortLinkDO, updateWrapper);
        } else {
            LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                    .eq(ShortLinkDO::getGid, hasShortLinkDO.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, reqParam.getFullShortUrl())
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0);
            baseMapper.delete(updateWrapper);
            shortLinkDO.setGid(reqParam.getGid());
            baseMapper.insert(shortLinkDO);
        }
    }

    /**
     * 短链接重定向处理函数
     *
     * @param shortUri 短链接标识符，如"abc123"
     * @param request  Servlet请求对象，用于获取服务端信息
     * @param response Servlet响应对象，用于执行重定向操作
     */
    @SneakyThrows
    @Override
    public void redirectShortLink(String shortUri, ServletRequest request, ServletResponse response) {
        // 构造完整短链接地址：域名+短码
        String serverName = request.getServerName();
        String fullShortUrl = serverName + "/" + shortUri;

        // 第一层缓存查询：直接尝试从Redis获取原始链接
        String originLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
        if (StrUtil.isNotBlank(originLink)) {
            shortLinkStats(fullShortUrl, null, request, response);
            ((HttpServletResponse) response).sendRedirect(originLink);
            return;
        }

        // 布隆过滤器校验：防止缓存击穿攻击
        boolean contains = shortLinkCreateCachePenetrationBloomFilter.contains(fullShortUrl);
        if (!contains) {
            shortLinkStats(fullShortUrl, null, request, response);
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }

        // 空值缓存检查：避免重复查询不存在的记录
        String gotoShortLinkIsNull = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_IS_NULL_KEY, fullShortUrl));
        if (StrUtil.isNotBlank(gotoShortLinkIsNull)) {
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }

        // 获取分布式锁：防止缓存击穿（双重检查锁模式）
        RLock lock = redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));
        lock.lock();
        try {
            // 二次缓存检查：防止其他线程已更新缓存
            originLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
            if (StrUtil.isNotBlank(originLink)) {
                shortLinkStats(fullShortUrl, null, request, response);
                ((HttpServletResponse) response).sendRedirect(originLink);
                return;
            }

            // 数据库查询阶段：通过路由表关联原始链接
            LambdaQueryWrapper<ShortLinkGotoDO> shortLinkGotoDOLambdaQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                    .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
            ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(shortLinkGotoDOLambdaQueryWrapper);

            // 设置空值缓存：防止后续相同请求穿透到数据库
            if (shortLinkGotoDO == null) {
                stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_IS_NULL_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);
                ((HttpServletResponse) response).sendRedirect("/page/notfound");
                return;
            }

            // 查询有效短链接信息（包含删除状态和启用状态校验）
            LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getFullShortUrl, shortLinkGotoDO.getFullShortUrl())
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid());
            ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);

            // 缓存回填并执行重定向
            if (shortLinkDO == null || shortLinkDO.getValidDate().before(new Date()) && shortLinkDO.getValidDate() != null) {
                stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_IS_NULL_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);
                ((HttpServletResponse) response).sendRedirect("/page/notfound");
                return;
            }
            stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                    shortLinkDO.getOriginUrl(),
                    LinkUtil.getLinkValidTime(shortLinkDO.getValidDate()),
                    TimeUnit.MINUTES);
            shortLinkStats(fullShortUrl, shortLinkDO.getGid(), request, response);
            ((HttpServletResponse) response).sendRedirect(shortLinkDO.getOriginUrl());
        } finally {
            // 确保锁释放
            lock.unlock();
        }
    }

    public void shortLinkStats(String fullShortUrl, String gid, ServletRequest request, ServletResponse response) {
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        uvFirstFlag.set(Boolean.TRUE);
        Cookie[] cookies = ((HttpServletRequest) request).getCookies();
        //统计uv
        try {
            AtomicReference<String> uv = new AtomicReference<>();
            uv.set(UUID.fastUUID().toString());
            Runnable runnable = () -> {
                Cookie uvcookie = new Cookie("uv", uv.get());
                uvcookie.setMaxAge(60 * 60 * 24 * 30);
                uvcookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.indexOf("/"), fullShortUrl.length()));
                ((HttpServletResponse) response).addCookie(uvcookie);
                stringRedisTemplate.opsForSet().add("short-link:uv:stats" + fullShortUrl, uv.get());
            };
            if (ArrayUtil.isNotEmpty(cookies)) {
                Arrays.stream(cookies).filter(each -> Objects.equals(each.getName(), "uv"))
                        .findFirst()
                        .map(Cookie::getValue)
                        .ifPresentOrElse(each -> {
                            uv.set(each);
                            Long uvAdded = stringRedisTemplate.opsForSet().add("short-link:uv:stats" + fullShortUrl, each);
                            uvFirstFlag.set(uvAdded != null && uvAdded > 0L);
                        }, runnable);
            } else {
                runnable.run();
            }
            //统计ip
            String actualIp = LinkUtil.getActualIp((HttpServletRequest) request);
            Long ipAdded = stringRedisTemplate.opsForSet().add("short-link:uv:stats" + fullShortUrl, actualIp);
            boolean uipFirstFlag = ipAdded != null && ipAdded > 0L;
            if (StrUtil.isBlank(gid)) {
                LambdaQueryWrapper<ShortLinkGotoDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                        .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
                ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(queryWrapper);
                gid = shortLinkGotoDO.getGid();
            }
            int hour = DateUtil.hour(new Date(), true);
            Week week = DateUtil.dayOfWeekEnum(new Date());
            int weekday = week.getIso8601Value();
            LinkAccessStatsDO linkAccessStatsDO = LinkAccessStatsDO.builder()
                    .hour(hour)
                    .weekday(weekday)
                    .pv(1)
                    .uip(uipFirstFlag ? 1 : 0)
                    .uv(uvFirstFlag.get() ? 1 : 0)
                    .gid(gid)
                    .fullShortUrl(fullShortUrl)
                    .date(new Date())
                    .build();
            linkAccessStatsMapper.shortLinkStats(linkAccessStatsDO);
            //统计地区
            Map<String, Object> localeStatsReqMap = new HashMap<>();
            localeStatsReqMap.put("key", AMAP_KEY);
            localeStatsReqMap.put("ip", actualIp);
            String localeResult = HttpUtil.get(SHORT_LINK_REMOTE_URL, localeStatsReqMap);
            JSONObject localeJsonResult = JSON.parseObject(localeResult);
            String RemoteCode = localeJsonResult.getString("infocode");
            String province = localeJsonResult.getString("province");
            if (StrUtil.isNotBlank(RemoteCode) && Objects.equals(RemoteCode, "10000")) {
                boolean unKnownProvince = StrUtil.equals(province, "[]");
                LinkLocaleStatsDO linkLocaleStatsDO = LinkLocaleStatsDO.builder()
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .date(new Date())
                        .province(unKnownProvince ? "未知" : province)
                        .city(unKnownProvince ? "未知" : localeJsonResult.getString("city"))
                        .adcode(unKnownProvince ? "未知" : localeJsonResult.getString("adcode"))
                        .country("中国")
                        .cnt(1)
                        .build();
                linkLocaleStatsMapper.shortLinkStats(linkLocaleStatsDO);
                //统计操作系统访问次数
                String osStats = LinkUtil.getOs((HttpServletRequest) request);
                if (StrUtil.isNotBlank(osStats)) {
                    LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
                            .gid(gid)
                            .fullShortUrl(fullShortUrl)
                            .date(new Date())
                            .os(osStats)
                            .cnt(1)
                            .build();
                    linkOsStatsMapper.shortLinkStats(linkOsStatsDO);
                }
                //统计浏览器访问次数
                LinkBrowserStatsDO linkBrowserStatsDO = LinkBrowserStatsDO.builder()
                        .browser(LinkUtil.getBrowser(((HttpServletRequest) request)))
                        .cnt(1)
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .date(new Date())
                        .build();
                linkBrowserStatsMapper.shortLinkBrowserState(linkBrowserStatsDO);
                //统计高频ip
                //TODO 优化
                LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
                        .user(uv.get())
                        .fullShortUrl(fullShortUrl)
                        .ip(actualIp)
                        .browser(LinkUtil.getBrowser(((HttpServletRequest) request)))
                        .os(LinkUtil.getOs((HttpServletRequest) request))
                        .build();
                linkAccessLogMapper.insert(linkAccessLogsDO);
                LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
                        .device(LinkUtil.getDevice(((HttpServletRequest) request)))
                        .cnt(1)
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .date(new Date())
                        .build();
                linkDeviceStatsMapper.shortLinkDeviceState(linkDeviceStatsDO);
                LinkNetworkStatsDO linkNetworkStatsDO = LinkNetworkStatsDO.builder()
                        .network(LinkUtil.getNetwork(((HttpServletRequest) request)))
                        .cnt(1)
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .date(new Date())
                        .build();
                linkNetworkStatsMapper.shortLinkNetworkState(linkNetworkStatsDO);
            }
        } catch (Exception e) {
            log.info("统计失败", e);
        }
    }

    public String generateSuffix(ShortLinkCreateReqDTO reqParam) {
        int custom = 0;
        String shortUri;
        while (true) {
            if (custom > 10) {
                throw new ServiceException("创建短链接失败");
            }
            String originUrl = reqParam.getOriginUrl();
            originUrl += System.currentTimeMillis();
            shortUri = HashUtil.hashToBase62(originUrl);
            if (!shortLinkCreateCachePenetrationBloomFilter.contains(reqParam.getDomain() + '/' + shortUri)) {
                break;
            }
            custom++;
        }
        return shortUri;
    }

    /**
     * 获取网站的favicon图标链接
     *
     * @param url 网站的URL
     * @return favicon图标链接，如果不存在则返回nulL
     */
    @SneakyThrows
    private String getFavicon(String url) {
        URL targetUrl = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();
        connection.setInstanceFollowRedirects(false);
        connection.setRequestMethod("GET");
        connection.connect();
        int responseCode = connection.getResponseCode();
        if (responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_MOVED_TEMP) {
            String redirectUrl = connection.getHeaderField("Location");
            if (redirectUrl != null) {
                URL newUrl = new URL(redirectUrl);
                connection = (HttpURLConnection) newUrl.openConnection();
                connection.setRequestMethod("GET");
                connection.connect();
                responseCode = connection.getResponseCode();
            }
        }
        if (responseCode == HttpURLConnection.HTTP_OK) {
            Document document = Jsoup.connect(url).get();
            Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first();
            if (faviconLink != null) {
                return faviconLink.attr("abs:href");
            }
        }
        return null;
    }
}